fix(codegen): emit tensor phis via tensors dict to avoid NameError (#2180) - #2183
Conversation
When ConvertToSSA synthesizes phi `return_vars_` for cross-branch diverging tensor variables, host_orch.py codegen must pre-declare the phi name in the `tensors` dict and emit yield-to-phi assignments that reference tensors-dict names — not bare Python variables. Fixes hw-native-sys#2180. - IfStmt visitor (DistributedCodegen): pre-declares phi variables before `if`, emits branch-specific yield-to-phi `tensors[…] = tensors[…]` assignments, and traces Var→Var aliases (kernel param copies) to the original tensors-dict name. - AssignStmt visitor: aliases `tensor_var = kernel_param` are emitted as `tensors["tensor_var"] = tensors["kernel_param"]` instead of bare Python names. - Unit test validates phi pre-declaration, yield assignments, and absence of bare-name tensor assignments. Co-authored-by: georgebisbas <georgios.bismpas@h-partners.com> Co-authored-by: vloncar <vloncar@users.noreply.github.com>
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughDistributed codegen now preserves tensor aliases through the tensors registry and emits SSA phi declarations and branch assignments for conditional control flow. Tests optionally run SSA conversion and verify generated host orchestration code for cross-branch tensor values. ChangesDistributed tensor phi handling
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ecbf7e1acd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| VisitExpr(src); | ||
| tensor_phi_init = current_expr_value_; |
There was a problem hiding this comment.
Avoid emitting branch calls while probing phi init
When a HOST orchestrator has no tensor formal parameters, this fallback tries to derive a tensor phi initializer by visiting the then-branch assignment RHS before emitting the if. If that RHS is a hierarchy call such as boundary = self.chip_run(tmp) (where tmp is a top-level created tensor), VisitExpr(src) calls EmitCallToWorker, so the generated host_orch.py submits that chip task unconditionally before the branch condition is evaluated, and with no assignment target for the call output. That changes program behavior for no-input host orchestrators with tensor phis; initializer discovery here needs to avoid non-pure expression visitors and only use already-materialized tensor names.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 8e0e5e0: both init-discovery paths now guard VisitExpr with ir::As<ir::Var>(src) != nullptr so only Var→Var aliases are visited. For Call RHS the init falls through to the shared tensor_phi_init (function params) or zero placeholder — EmitCallToWorker is never triggered before the if condition.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/codegen/distributed/distributed_codegen.cpp`:
- Around line 869-909: Update the tensor-phi handling in the pre-declaration
loop over op->return_vars_ so each tensor phi derives its initializer from that
phi’s own in-scope source or pre-if tensor, rather than reusing the single
tensor_phi_init selected earlier. Preserve the initializer’s shape and dtype,
and only use the fallback placeholder when no valid source exists for that
specific phi.
In `@tests/ut/codegen/distributed/test_host_orch_distributed.py`:
- Around line 959-1020: Strengthen the then_yield assertion in
test_if_cross_branch_phi_predeclares_and_yields_tensors so it examines only the
generated then-branch body, excluding the pre-if phi declaration. Isolate the
region between the if and else boundaries (or otherwise anchor the match after
the if) and require the tensors-based phi assignment there, so the assertion
fails if then-branch emission regresses.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6a4b9206-d633-4f0b-a013-142cf86d5458
📒 Files selected for processing (2)
src/codegen/distributed/distributed_codegen.cpptests/ut/codegen/distributed/test_host_orch_distributed.py
…-sys#2183) CodeRabbit review: each tensor-typed phi now derives its pre-declaration initializer from its own yield source instead of a shared init. Test assertion isolates the then-branch body from the pre-if declaration. Pre-commit: suppress pyright reportReturnType for IR-level return 0.
…w-native-sys#2183) When a HOST orchestrator has no tensor formal parameters, the phi init fallback visited the then-branch yield source via VisitExpr. If that source is a hierarchy call, EmitCallToWorker would emit a _submit_chip before the `if` condition, altering program semantics. Only resolve Var→Var aliases now; Call RHS falls through to the zero placeholder. Co-authored-by: georgebisbas <georgios.bismpas@h-partners.com> Co-authored-by: vloncar <vloncar@users.noreply.github.com>
ReviewThe diagnosis and the fix direction are right: in this codegen a tensor only ever exists as a My main comment is that the ~140 new lines in 1.
|
| definition of the yield var | is tensors[<name>] populated? |
|---|---|
| Var→Var tensor alias | ✅ the new AssignStmt branch in this PR |
| Call RHS, callee has Out/InOut params | ✅ EmitCallToWorker, line 1130 |
| Call RHS, no Out params | ✅ EmitCallToWorker, lines 1068–1070 |
tensor.create |
✅ EmitTensorCreate, line 1194 |
TupleGetItemExpr unpacking |
✅ line 713 |
| function parameter | ✅ seeded by the runtime |
| hoisted alloc | ✅ _alloc_intermediates already filled it |
So by the time VisitStmt(op->then_body_) returns, tensors[<yield var name>] is guaranteed to exist and the yield can be emitted directly, with no RHS back-tracing.
The pre-declaration is redundant for the same reason: ConvertToSSA always synthesizes an else body when it creates phis (convert_to_ssa_pass.cpp:936-939, else_with_yield = make_shared<YieldStmt>(else_yields, ...)), so both branches always assign the phi. That also removes the two fallback initializers, which are the part I'd most like to see gone:
torch.zeros((1,), dtype=torch.float32)hardcodes shape and dtype, while the phi's ownTensorTypeis right there (compare lines 1051–1070, which derive shape/dtype from the return type).tensor_phi_initpicks the function's first tensor parameter, which is unrelated to the phi in shape, dtype and meaning.
Neither should ever fire, and if one does it produces a silently wrong tensor rather than a loud failure.
Concretely, the whole visitor could be:
void DistributedCodegen::VisitStmt_(const ir::IfStmtPtr& op) {
INTERNAL_CHECK(op != nullptr) << "Internal error: null IfStmt";
// ConvertToSSA always appends an else carrying the phi's incoming values
// (convert_to_ssa_pass.cpp:936-939), so a phi is always defined on both paths.
INTERNAL_CHECK_SPAN(op->return_vars_.empty() || op->else_body_.has_value(), op->span_)
<< "Internal error: IfStmt with return_vars_ must carry an else_body "
"holding the phi's incoming values";
VisitExpr(op->condition_);
const std::string condition = current_expr_value_;
current_expr_value_ = "";
// Merge each branch's yield into the phi name so post-if consumers see one
// name instead of a branch-local SSA name (issue #2180).
auto emit_yields = [&](const ir::StmtPtr& body) {
const auto yld =
ir::transform_utils::GetLastYieldStmt(ir::transform_utils::UnwrapAutoScope(body));
if (!yld) return;
for (size_t i = 0; i < op->return_vars_.size() && i < yld->value_.size(); ++i) {
VisitExpr(yld->value_[i]);
const std::string val = current_expr_value_;
current_expr_value_ = "";
const std::string phi = SanitizeName(op->return_vars_[i]->name_hint_);
if (ir::AsTensorTypeLike(op->return_vars_[i]->GetType())) {
emitter_.EmitLine("tensors[\"" + phi + "\"] = tensors[\"" + val + "\"]");
} else {
emitter_.EmitLine(phi + " = " + val);
}
declared_vars_.insert(phi);
}
};
emitter_.EmitLine("if " + condition + ":");
emitter_.IncreaseIndent();
VisitStmt(op->then_body_);
emit_yields(op->then_body_);
emitter_.DecreaseIndent();
if (op->else_body_.has_value()) {
emitter_.EmitLine("else:");
emitter_.IncreaseIndent();
VisitStmt(*op->else_body_);
emit_yields(*op->else_body_);
emitter_.DecreaseIndent();
}
}This also merges the phi and non-phi paths (the return_vars_.empty() loop is a no-op), so there's no duplicated if/else emission, and it drops the transform_utils.h include down to just the two helpers actually used.
Side benefit: find_yield_source calls FlattenToStmts once per phi, and is called from both the init loop and the yield loop, so the current shape is O(P·N) per IfStmt (times nesting depth). .claude/rules/pass-complexity.md asks for O(N log N); the version above is a single linear pass.
2. The else_body_ check is not just defensive
Worth keeping the INTERNAL_CHECK_SPAN above, because the frontend can produce return_vars_ without an else. parse_if_statement (ast_parser.py:3050) creates return_vars_ from explicit pl.yield_() calls (if_builder.return_var(...), lines 3105–3113) but only calls if_builder.else_() when stmt.orelse exists (line 3096). So:
if r == 0:
pl.yield_(boundary=zero) # no elseyields IfStmt(return_vars_=[boundary], else_body_=nullopt). ConvertToSSA does not repair it either: since zero is a parameter that is never re-versioned inside the branch, then_ver == before and phis comes out empty (lines 843–860), which takes the pass-through path at lines 876–893 where new_else stays nullopt. The else synthesis at 936–939 is only reached when phis is non-empty.
Today that shape silently emits the (1,)/float32 placeholder; without the pre-declaration it would regress to the original KeyError. An explicit internal check turns it into a clear compiler-bug message. (I did not check whether a verifier already rejects this shape — if it does, the check is just cheap insurance.)
3. As<Var> misses IterArg in the AssignStmt branch
This one survives the simplification, since it's the core fix:
if (ir::AsTensorTypeLike(op->var_->GetType()) && ir::As<ir::Var>(op->value_)) {As<T>() is an exact ObjectKind match, so IterArg doesn't hit it (see .claude/rules/ir-kind-traits.md). Loop-carried tensors from pl.range(..., init_values=[...]) are IterArgs, and they'd fall through to the old bare-name path — the same bug this PR fixes, via a different door. Suggest ir::AsVarLike(op->value_).
Pre-existing and out of scope, but related: DistributedCodegen overrides VisitExpr_(const ir::VarPtr&) rather than VisitVarLike_, while functor.h:111 dispatches IterArg to VisitExpr_(IterArgPtr) → the base IRVisitor::VisitVarLike_ (visitor.h:48-52), which never sets current_expr_value_. So even with AsVarLike, VisitExpr(iter_arg) returns an empty string. If loop-carried tensors are meant to work, VisitExpr_(VarPtr) should become VisitVarLike_; otherwise it's worth a follow-up issue.
4. The AsTensorTypeLike guard over-matches DistributedTensorType
AsTensorTypeLike matches both TensorType and DistributedTensorType (kind_traits.h:329). The distributed_tensor_alias early return above only fires when both sides are DistributedTensorType; a DistributedTensorType var with a plain-TensorType Var value would reach the new branch and emit tensors[x] = tensors[y], whereas DistributedTensors are resolved through window_buffer_ (lines 1005–1008), not the tensors dict. Exact ir::As<ir::TensorType>(op->var_->GetType()) here would be safer. (Distributed-tensor phis aren't handled by either the old or the new code — probably fine to leave, but worth a comment.)
5. Test
Good that it's a real before/after with assertions, in the right place, and that _lower(..., convert_to_ssa=False) keeps existing callers untouched. Two things:
code.split("if", 1)splits on the substringif, which matches comments, identifiers, and — most relevant here — theif "<target>" not in tensors:line thatEmitCallToWorkeremits at line 1068 for a callee with no Out params, which is exactly whatchip_runis in this test. It happens to pass today, but any preamble change can silently move the split point. A line-anchoredre.search(r"^\s*if .*:$", line, re.M)would be robust.- Since the reported symptom is "generated module fails at
prepare()", acompile(code, "<generated>", "exec")assertion (or anast.parse) covers the bug more directly than the name regexes.
If you keep the simplified version, a scalar-phi case and an if + pl.yield_() without else case would cover the two paths flagged above cheaply.
Overall: the fix is correct, CI is green, and the risk is confined to the distributed codegen path. My suggestion is to land the AssignStmt change plus the ~25-line IfStmt version, which removes the fallback initializers and the O(P·N) scan along with it.
Drop the pre-declaration loop, find_yield_source helper, and fallback initializers (torch.zeros placeholder and tensor_phi_init) from VisitStmt_(IfStmtPtr). The AssignStmt fix (also in this branch) guarantees that tensors[] is populated for every yield var by the time VisitStmt returns, so yields can be emitted directly with no RHS back-tracing. Also tighten the AssignStmt tensor-alias guard: use AsVarLike (vs As<Var>) to catch IterArg loop-carried tensors, and exact As<TensorType> (vs AsTensorTypeLike) to avoid over-matching DistributedTensorType. Test: replace fragile substring split with line-anchored if/else matching and add compile() sanity check. Rename to reflect simplified behavior.
I can't authenticate
1. Simplified 2. 3. Exact 4. Test robustness All 8015 unit tests, 637 codegen tests, and 125 SSA tests pass. No existing test exercises the |
Summary
Fixes #2180: cross-branch phi
NameErrorinhost_orch.pyatprepare()time.When
ConvertToSSAsynthesizes phireturn_vars_for cross-branch diverging tensor variables, the distributed codegen now:tensorsdict before theifblocktensors[...] = tensors[...](not bare Python names)tensors-dict referenceAssignStmts astensors["var"] = tensors["value"]instead of bare Python variable namesTest plan
test_if_cross_branch_phi_predeclares_and_yields_tensorsvalidates:ifblocktensors[...]host_orch.pyconfirmed to produce valid, executable code that no longer raisesNameErrorCo-authored-by: @georgebisbas @vloncar